'''
Created on 15.06.2012

@author: dominik
'''
from collections import namedtuple
import cairo
from cairo import ImageSurface, Context
from drawing import FontManager

Size = namedtuple("Size", ["width", "height"])
Point = namedtuple("Point", ["x", "y"])

class PrototypeRenderer(object):
    '''
    Prototype for a renderer that draws sequence charts.
    '''

    def __init__(self, actors, actions, theme):
        '''
        Constructor
        '''
        if not actors:
            raise ValueError, "Actors list may not be empty."
        if actions is None:
            raise ValueError, "Actions list may not be None."

        self.actors = actors
        self.actions = actions
        self.theme = theme
        self.sizes = {}
        self.positions = {}
        self.directions = {}
        self.fontManager = FontManager(theme, None)

    def get_image_surface(self):
        """
        Generate the image for the model.
        """
        self.calc_dimensions()
        
        surface = ImageSurface(cairo.FORMAT_ARGB32,
                                     self.sizes["image"].width,
                                     self.sizes["image"].height)
        ctx = Context(surface)

        self.fontManager = FontManager(self.theme, ctx)

        # draw a background
        self.draw_background(ctx)

        ctx.translate(self.theme.IMAGE_PADDING, self.theme.IMAGE_PADDING)
        self.draw_actor_line(ctx)

        start_y = self.positions["action_lines.start.y"]
        end_y = self.positions["action_lines.end.y"]

        ctx.save()
        ctx.set_source_rgb(0,0,0)
        for actor in self.actors:
            ctx.save()
            pos = self.positions["actor."+actor+".center"]
            ctx.translate(pos.x, 0)
            actor_width = self.sizes["actor."+actor].width

            ctx.move_to(0, start_y)
            ctx.line_to(0, end_y)
            ctx.stroke()
            ctx.restore()

        ctx.restore()
        ctx.save()
        ctx.translate(0, start_y)
        for action in self.actions:
            self.draw_action_line(ctx, action)
            ctx.translate(0, self.sizes["action."+action.id].height)
        ctx.restore()
        ctx.translate(0, end_y)
        self.draw_actor_line(ctx)

        return surface

    def draw_background(self, ctx):
        ctx.rectangle(0, 0, self.sizes["image"].width, self.sizes["image"].height)
        ctx.set_source_rgb(1,1,1)
        ctx.fill()

    def draw_action_line(self, ctx, action):
        pos = self.positions["action."+action.id]
        size = self.sizes["action."+action.id]
        line_height = size.height - self.theme.ACTION_PADDING_Y
        right = pos.x + size.width
        ctx.set_source_rgb(0,0,0)
        ctx.move_to(pos.x, line_height)
        ctx.line_to(right, line_height)
        
        direction = self.directions["action."+action.id]
        if direction is "left":
            ctx.move_to(pos.x+5, line_height-5)
            ctx.line_to(pos.x+1, line_height)
            ctx.line_to(pos.x+5, line_height+5)
        else:
            ctx.move_to(right-5, line_height-5)
            ctx.line_to(right, line_height)
            ctx.line_to(right-5, line_height+5)
        ctx.stroke()
        
        string_size = self.fontManager.measure("text", action.text)
        ctx.rectangle((size.width - string_size[0])/2 + pos.x, 0, string_size[0], string_size[1])
        ctx.set_source_rgb(1,1,1)
        ctx.fill()
        ctx.set_source_rgb(0,0,0)
        ctx.move_to((size.width - string_size[0])/2 + pos.x, 0)
        self.fontManager.draw("text", action.text)

    def draw_actor_line(self, ctx):
        """
        Draw a line of actors in the context at the current (0,0).
        """
        for actor in self.actors:
            pos = self.positions["actor."+actor]

            ctx.save()
            ctx.set_source_rgb(0,0,0)
            ctx.translate(pos.x, pos.y)
            self.draw_single_actor(ctx, actor)
            ctx.restore()

    def draw_single_actor(self, ctx, name):
        """ Draw the box with label for a single actor. """
        size = self.sizes["actor."+name]
        ctx.rectangle(0, 0, size.width, size.height)
        ctx.stroke()
        ctx.move_to(self.theme.ACTOR_PADDING_X, self.theme.ACTOR_PADDING_Y)
        self.fontManager.draw("head", name)

    def calc_dimensions(self):
        """
        Iterates over the complete model and calculates various sizes and
        positions that are used for the drawing process.

        Fills self.sizes and self.positions.
        """
        self.positions["actor_line.top"] = Point(0, 0)

        self.calc_actor_sizes()
        self.calc_actor_positions()
        self.calc_action_positions()

        width = self.positions["actor."+self.actors[-1]+".center"].x + \
                self.sizes["actor."+self.actors[-1]].width + \
                self.theme.IMAGE_PADDING * 2

        height = self.theme.IMAGE_PADDING * 2
        height += 40 * 2 # The actor row is at top and bottom
        for action in self.actions:
            height += self.sizes["action."+action.id].height

        self.sizes["image"] = Size(width, height + 30)

        bottom_actor_y = self.sizes["image"].height
        bottom_actor_y -= self.theme.IMAGE_PADDING
        bottom_actor_y -= 40
        self.positions["actor_line.bottom"] = Point(self.theme.IMAGE_PADDING,
                                               bottom_actor_y)
        
        self.positions["action_lines.start.y"] = 40
        self.positions["action_lines.end.y"] = bottom_actor_y

    def calc_action_positions(self):
        for action in self.actions:
            actor1 = self.positions["actor."+action.from_actor+".center"]
            actor2 = self.positions["actor."+action.to_actor+".center"]
            if actor1.x < actor2.x:
                pos_x = actor1.x
                self.directions["action."+action.id] = "right"
            else:
                pos_x = actor2.x
                self.directions["action."+action.id] = "left"
            width = max(actor1.x, actor2.x) - pos_x
            self.positions["action."+action.id] = Point(pos_x, 0)
            s_height = self.fontManager.measure("text", action.text)[1]
            s_height += self.theme.ACTION_PADDING_Y * 2
            self.sizes["action."+action.id] = Size(width, s_height)
            
            

    def calc_actor_positions(self):
        """
        Calculate the positions by determining the relative distances needed
        between the actors caused by the widest action.text between them.
        
        The result is for each actor:
          self.positions[actors.actor_name.center] = Point where the actor-box has
                  its center. Used especially by the vertical actor-lines.
          self.positions[actors.actor] = Top-left point of the actor 
        """
        actor_widths = {}
        for action in self.actions:
            # This orders the two actors alphabetically, so that a->b and b->a both
            # become a->b. This is used to identify all actions that happen
            # between these.
            sorted_actors = "+".join(sorted((action.from_actor, action.to_actor)))
            width = self.fontManager.measure("text", action.text)[0]
            width += self.theme.ACTION_PADDING * 2
            if sorted_actors in actor_widths:
                actor_widths[sorted_actors] = max(actor_widths[sorted_actors], width)
            else:
                actor_widths[sorted_actors] = width
        
        # Calculate the center position for each actor by calculating the width
        # of the widest action-text that exists between the two. Also places the
        # actors from left to right in order as they appear in self.actors.
        prev_actor = None
        last_center = 0
        for actor in self.actors:
            actor_size = self.sizes["actor."+actor]
            if prev_actor is None:
                self.positions["actor."+actor+".center"] = Point(actor_size.width / 2, actor_size.height / 2)
                self.positions["actor."+actor] = Point(0,0)
                prev_actor = actor
                continue

            # The proposed center places the next actor with ACTOR_MARGIN_X pixels
            # between the left border of the previous actor and the right border of
            # the current one.
            proposed_center_x = self.sizes["actor."+prev_actor].width / 2
            proposed_center_x += self.sizes["actor."+actor].width / 2
            proposed_center_x += self.theme.ACTOR_MARGIN_X
            
            # When that calculated minimum size is too narrow to contain
            # the biggest action, take the width that this action needs.
            actors_key = "+".join(sorted((prev_actor, actor)))
            if actors_key not in actor_widths:
                minimum_needed_width = self.theme.ACTOR_MARGIN_X
            else:
                minimum_needed_width = actor_widths[actors_key]
            new_center_x = self.positions["actor."+prev_actor+".center"].x
            if proposed_center_x < minimum_needed_width:
                new_center_x += minimum_needed_width
            else:
                new_center_x += proposed_center_x
            
            self.positions["actor."+actor+".center"] = Point(new_center_x, actor_size.height / 2)
            self.positions["actor."+actor] = Point(new_center_x - actor_size.width / 2, 0)
            
            prev_actor = actor

    def calc_actor_sizes(self):
        """
        Calculate the sizes for each actor-box.
        """
        max_height = None
        all_same_height = True
        for actor in self.actors:
            string_size = self.fontManager.measure("head", actor)
            actor_width = self.theme.ACTOR_PADDING_X * 2
            actor_width += string_size[0]

            actor_height = self.theme.ACTOR_PADDING_Y * 2
            actor_height += string_size[1]

            if max_height is None:
                max_height = actor_height
            elif max_height > actor_height:
                max_height = actor_height
                all_same_height = False
            self.sizes["actor."+actor] = Size(actor_width, actor_height)
        
        if not all_same_height:
            for actor in self.actors:
                size = self.sizes["actor."+actor]
                self.sizes["actor."+actor] = Size(size.width, max_height)
